package rmiClient;

import interfaces.Client_ITF;
import interfaces.Serveur_ITF;

import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import view.Transmit;

/**
 * @author SBAI Kévin and CARTIER Arthur
 * 
 */
public class Client_IMPL extends UnicastRemoteObject implements Client_ITF {

	/**
	 * @author SBAI Kévin and CARTIER Arthur
	 * 
	 *         This class represent one instance of a thread in charge of asking
	 *         a friend for an election But not used yet, we will add an
	 *         optional second election method button later
	 * 
	 */
	public class election implements Future<Score>, Callable<Score> {

		Client_ITF friendToAsk;
		String electionId;
		Score result;
		boolean isDone;

		public election(Client_ITF friendToAsk, String electionId) {
			super();
			this.friendToAsk = friendToAsk;
			this.electionId = electionId;
		}

		@Override
		public Score call() throws Exception {
			elect();
			return result;
		}

		@Override
		public boolean cancel(boolean mayInterruptIfRunning) {
			// TODO Auto-generated method stub
			return false;
		}

		public void elect() {
			try {
				result = friendToAsk.election(electionId);
			} catch (RemoteException e) {
				result = new Score();
			}
		}

		@Override
		public Score get() throws InterruptedException, ExecutionException {
			return result;
		}

		@Override
		public Score get(long timeout, TimeUnit unit)
				throws InterruptedException, ExecutionException,
				TimeoutException {
			return result;
		}

		@Override
		public boolean isCancelled() {
			// TODO Auto-generated method stub
			return false;
		}

		@Override
		public boolean isDone() {
			return isDone;
		}

	}

	/**
	 * 
	 */
	private static final long serialVersionUID = 6465134064309852157L;

	HashMap<Integer, Client_ITF> neighbors;
	ArrayList<String> electionsHistory;
	Serveur_ITF server;
	int id;
	int scoreLocal;
	String Ip;
	String MoviesFolder;
	ArrayList<String[]> electionWithoutDiffusionResult;
	Transmit t;
	Client_ITF emettor;
	boolean transmitionEnCours = false;

	public Client_IMPL(Serveur_ITF server, int id, int score, String ip,
			String moviesFolder) throws RemoteException {
		super();
		this.server = server;
		this.id = id;
		neighbors = new HashMap<Integer, Client_ITF>();
		scoreLocal = score;
		electionsHistory = new ArrayList<String>();
		Ip = ip;
		MoviesFolder = moviesFolder;
		electionWithoutDiffusionResult = new ArrayList<String[]>();
	}

	/**
	 * @see interfaces.Client_ITF#addNeighbor(interfaces.Client_ITF)
	 * @param newNeighbors
	 *            the neighbor to add
	 */
	@Override
	public void addNeighbor(Client_ITF newNeighbor) throws RemoteException {
		neighbors.put(newNeighbor.getId(), newNeighbor);
	}

	public void display_neighbors() throws RemoteException {
		int i = 1;

		System.out.println("Client " + id + " neighbors list : ");
		for (Client_ITF neighbor : neighbors.values()) {
			System.out.println("\tNeighbor number : " + i + "\tid : "
					+ (neighbor).getId());
			i++;
		}
		System.out.print("\n");
	}

	/**
	 * @see interfaces.Client_ITF#election(java.lang.String)
	 */
	@Override
	public Score election(String p_electionId) throws RemoteException {
		Score res = null;
		String electionId;
		if (p_electionId == null || p_electionId.isEmpty()) {
			// initiate the election
			electionId = "" + id + "-" + (new Date()).getTime();
		} else {
			electionId = p_electionId;
		}

		if (electionsHistory.contains(electionId)) {
			// election already computed return a fake score
			res = new Score();
		} else {
			// proced to the election but first record the electionId !!!

			electionsHistory.add(electionId);

			ArrayList<Future<Score>> friendsResults = new ArrayList<Future<Score>>();
			ExecutorService executor = Executors.newCachedThreadPool();
			for (Client_ITF currentClient : neighbors.values()) {
				Future<Score> calculateur = new election(currentClient,
						electionId);
				friendsResults.add(executor
						.submit((Callable<Score>) calculateur));
			}
			res = new Score(scoreLocal, this);
			// TODO allow to check results in the same order we get them
			for (Future<Score> currentResult : friendsResults) {
				try {
					if (res.compareTo(currentResult.get()) < 0) {
						res = currentResult.get();
					}
				} catch (InterruptedException e) {
					// if this thread has been stopped we just gona take an
					// other result
				} catch (ExecutionException e) {
					// if an error occurs there is probably an error on the
					// friend we are asking so we gona choose an other
				}
			}
		}
		return res;
	}

	@Override
	public void electionWithoutDiffusion(Token token) throws RemoteException {

		if (!token.alreadyInterrogated.contains(getId())) {
			if (!transmitionEnCours) {
				if (token.getBestScore().getScore() > getScoreLocal()) {
					token.setBestScore(new Score(getScoreLocal(), this));
				}
			}
			token.getAlreadyInterrogated().insertElementAt(getId(), 0);
		}

		if (!token.getAlreadyInterrogated()
				.containsAll(getNeighbors().keySet())) {
			// send the Token to the first neighbors who is not in the
			// AlreadyInterrogated List
			Iterator<Integer> myNeighbors = getNeighbors().keySet().iterator();
			int currentNeighborId = 0;
			boolean found = false;
			while (!found && myNeighbors.hasNext()) {
				currentNeighborId = myNeighbors.next();
				if (!token.getAlreadyInterrogated().contains(currentNeighborId)) {
					found = true;
					getNeighbors().get(currentNeighborId)
							.electionWithoutDiffusion(token);
				}
			}

		} else if (token.getInitiator() == getId()) {
			// end of election
			String ipPort = token.getBestScore().getClient()
					.startStreamming(token.videoName);
			if (ipPort != null) {
				String[] newResult = new String[2];
				newResult[0] = "" + token.getBestScore().client.getId();
				newResult[1] = ipPort;
				electionWithoutDiffusionResult.add(newResult);
				emettor = token.getBestScore().getClient();
			}
		} else {
			// step back
			// we search the first neighbors who gets the Token before us
			Iterator<Integer> alreadyInterogatedClientsId = token
					.getAlreadyInterrogated().iterator();
			boolean foundMyself = false;
			boolean foundMyneighbors = false;
			while (!foundMyself && alreadyInterogatedClientsId.hasNext()) {
				if (getId() == alreadyInterogatedClientsId.next()) {
					foundMyself = true;
				}
			}

			while (!foundMyneighbors && alreadyInterogatedClientsId.hasNext()) {
				Integer currentId = alreadyInterogatedClientsId.next();
				if (getNeighbors().get(currentId) != null) {
					foundMyneighbors = true;
					getNeighbors().get(currentId).electionWithoutDiffusion(
							token);
				}
			}
		}

	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Client_ITF) {
			try {
				return id == ((Client_ITF) obj).getId();
			} catch (RemoteException e) {
				return false;
			}
		} else {
			return false;
		}
	}

	public ArrayList<String> getElectionsHistory() {
		return electionsHistory;
	}

	public ArrayList<String[]> getElectionWithoutDiffusionResult() {
		return electionWithoutDiffusionResult;
	}

	public Client_ITF getEmettor() {
		return emettor;
	}

	@Override
	public int getId() {
		return id;
	}

	@Override
	public String getInfo() throws RemoteException {
		StringBuilder sb = new StringBuilder("Info du client :\n");
		sb.append("Id : " + getId());
		sb.append("\n");
		sb.append("IP : " + getIp());
		sb.append("\n");
		sb.append("score : " + getScoreLocal());
		sb.append("\n");
		for (Client_ITF c : neighbors.values()) {
			sb.append("Id voisin : " + c.getId());
			sb.append("\n");
			sb.append("\tIP : " + c.getIp());
			sb.append("\n");
			sb.append("\tscore : " + c.getScoreLocal());
			sb.append("\n");
		}
		for (String election : electionsHistory) {
			sb.append("election : " + election);
			sb.append("\n");
		}
		return sb.toString();
	}

	@Override
	public String getIp() {
		return Ip;
	}

	public String getMoviesFolder() {
		return MoviesFolder;
	}

	@Override
	public HashMap<Integer, Client_ITF> getNeighbors() throws RemoteException {
		return neighbors;
	}

	@Override
	public int getScoreLocal() {
		return scoreLocal;
	}

	public Serveur_ITF getServer() {
		return server;
	}

	public Transmit getT() {
		return t;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = super.hashCode();
		result = prime * result + ((Ip == null) ? 0 : Ip.hashCode());
		result = prime * result
				+ ((MoviesFolder == null) ? 0 : MoviesFolder.hashCode());
		result = prime
				* result
				+ ((electionWithoutDiffusionResult == null) ? 0
						: electionWithoutDiffusionResult.hashCode());
		result = prime
				* result
				+ ((electionsHistory == null) ? 0 : electionsHistory.hashCode());
		result = prime * result + ((emettor == null) ? 0 : emettor.hashCode());
		result = prime * result + id;
		result = prime * result
				+ ((neighbors == null) ? 0 : neighbors.hashCode());
		result = prime * result + scoreLocal;
		result = prime * result + ((server == null) ? 0 : server.hashCode());
		result = prime * result + ((t == null) ? 0 : t.hashCode());
		result = prime * result + (transmitionEnCours ? 1231 : 1237);
		return result;
	}

	public boolean isTransmitionEnCours() {
		return transmitionEnCours;
	}

	/**
	 * @see interfaces.Client_ITF#removeNeighbor(interfaces.Client_ITF)
	 * @param neighbor
	 *            the neighbor to remove
	 */
	@Override
	public void removeNeighbor(Client_ITF neighbor) throws RemoteException {
		neighbors.remove(neighbor.getId());
		if (neighbors.size() < 2) {
			ArrayList<Client_ITF> newNeighbors = server.getNeighbors(this);
			for (Client_ITF newNeighbor : newNeighbors) {
				neighbors.put(newNeighbor.getId(), newNeighbor);
			}
		}
	}

	public void setElectionsHistory(ArrayList<String> electionsHistory) {
		this.electionsHistory = electionsHistory;
	}

	public void setElectionWithoutDiffusionResult(
			ArrayList<String[]> electionWithoutDiffusionResult) {
		this.electionWithoutDiffusionResult = electionWithoutDiffusionResult;
	}

	public void setEmettor(Client_ITF emettor) {
		this.emettor = emettor;
	}

	public void setId(int id) {
		this.id = id;
	}

	public void setIp(String ip) {
		Ip = ip;
	}

	public void setMoviesFolder(String moviesFolder) {
		MoviesFolder = moviesFolder;
	}

	public void setNeighbors(HashMap<Integer, Client_ITF> neighbors) {
		this.neighbors = neighbors;
	}

	public void setScoreLocal(int scoreLocal) {
		this.scoreLocal = scoreLocal;
	}

	public void setServer(Serveur_ITF server) {
		this.server = server;
	}

	public void setT(Transmit t) {
		this.t = t;
	}

	public void setTransmitionEnCours(boolean transmitionEnCours) {
		this.transmitionEnCours = transmitionEnCours;
	}

	@Override
	public String startStreamming(String videoName) throws RemoteException {
		String ipPort = Ip + "/";
		int port;
		port = (int) (Math.random() * (50000 - 2000)) + 2000;
		ipPort += port;
		// args pour la classe VideoTransmit
		String args[] = new String[3];
		args[0] = "file:";
		args[0] += getMoviesFolder();
		args[0] += '/';
		args[0] += videoName;
		args[1] = getIp();
		args[2] = "" + port;

		t = new Transmit(args, this);
		t.start();
		transmitionEnCours = true;

		return ipPort;
	}

	@Override
	public String stopStreamming() throws RemoteException {
		try {
			t.getVt().stop();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		transmitionEnCours = false;
		return null;
	}

}
